Created by miccall (转载请注明出处 miccall.tech)
1. OnRenderImage:
这个方法是unity在camera执行渲染时候被调用的一个 callback
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
SetShaderParameters();
Render(destination);
}
我们在这里执行两种操作,第一个是把我们定义的值传入到 computer shader 中让GPU执行,
执行SetShaderParameters 就是 向 computer shader 传入参数 。
传入的参数有
RWTexture2D<float4> Result;
float4x4 _CameraToWorld;
float4 _DirectionalLight;
float4x4 _CameraInverseProjection;
Texture2D <float4> _SkyboxTexture;
SamplerState sampler_SkyboxTexture;
float2 _PixelOffset;
float _Seed;
- Result 是一个可读写的图 ,由compute shader计算得到
- _CameraToWorld 是相机空间到世界空间的转化矩阵,我们为了方便,由自定义传入,而不是直接在shader里面计算
- _DirectionalLight 是光照方向,也是由世界空间的灯光信息传入
- _CameraInverseProjection 投影矩阵的逆矩阵,用于把相机空间的方向转化到世界空间
- _SkyboxTexture 是天空盒贴图
- _PixelOffset 我们设置的一个随机偏移量
- _Seed 是随机种子
有一部分我们可以预处理的时候传入,一部分我们要做一些运算才能传入:
private void SetShaderParameters()
{
RayTracingShader.SetFloat("_Seed", Random.value);
RayTracingShader.SetMatrix("_CameraToWorld", _camera.cameraToWorldMatrix);
RayTracingShader.SetMatrix("_CameraInverseProjection", _camera.projectionMatrix.inverse);
RayTracingShader.SetTexture(0, "_SkyboxTexture", skyboxTexture);
RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));
// light dir ;
var l = DirectionalLight.transform.forward;
RayTracingShader.SetVector("_DirectionalLight", new Vector4(l.x, l.y, l.z, DirectionalLight.intensity ));
// ****** important **********
RayTracingShader.SetBuffer(0, "_Spheres", _sphereBuffer);
}
第二个是渲染一个纹理 ,这个纹理最终会被camera 展示出来 。
Render 渲染一张图 :
private void Render(RenderTexture destination)
{
// current render target
InitRenderTexture();
// Set the target and dispatch to the compute shader
RayTracingShader.SetTexture(0, "Result", _target);
var threadGroupsX = Mathf.CeilToInt(Screen.width / 8.0f);
var threadGroupsY = Mathf.CeilToInt(Screen.height / 8.0f);
RayTracingShader.Dispatch(0, threadGroupsX, threadGroupsY, 1);
// Blit the result texture to the screen
if (_addMaterial == null)
_addMaterial = new Material(Shader.Find("Hidden/AddShader"));
_addMaterial.SetFloat(Sample, _currentSample);
// 通过 addshader 来做一个 抗锯齿
Graphics.Blit(_target, _converged, _addMaterial);
Graphics.Blit(_converged, destination);
_currentSample++;
}
2. compute shader path tracing
- Ray and create Ray
struct Ray
{
float3 origin;
float3 direction;
float3 energy;
};
Ray CreateRay(float3 origin, float3 direction)
{
Ray ray;
ray.origin = origin;
ray.direction = direction;
ray.energy = float3(1.0f, 1.0f, 1.0f);
return ray;
}
Ray CreateCameraRay(float2 uv)
{
// Transform the camera origin to world space
float3 origin = mul(_CameraToWorld, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz;
// 反转 view space 透视投影
float3 direction = mul(_CameraInverseProjection, float4(uv, 0.0f, 1.0f)).xyz;
// 将方向从摄 camera space 转换为 world space 并 normalize
direction = mul( _CameraToWorld, float4(direction, 0.0f)).xyz;
direction = normalize(direction);
return CreateRay(origin, direction);
}
CreateCameraRay 只会在最开始的时候调用一次,其余的光线追踪时反弹的光线我们直接用 CreateRay
- RayHit and CreateRayHit
struct RayHit
{
float3 position;
float distance;
float3 normal;
float3 albedo;
float3 specular;
float smoothness;
float3 emission;
};
RayHit CreateRayHit()
{
RayHit hit;
hit.position = float3(0.0f, 0.0f, 0.0f);
hit.distance = 1.#INF;
hit.normal = float3(0.0f, 0.0f, 0.0f);
hit.specular = float3(0.04f, 0.04f, 0.04f);
hit.albedo = min(1.0f - hit.specular, float3(0.8f, 0.8f, 0.8f));
hit.smoothness = 1.0f;
hit.emission = float3(0.0f,0.0f,0.0f);
return hit;
}
RayHit 代表光线击中的点 ,在 CreateRayHit 中做了初始化,在后续的过程中会被重新计算
- Trace and Shade
RayHit Trace(Ray ray)
{
RayHit bestHit = CreateRayHit();
IntersectGroundPlane(ray, bestHit);
uint numSpheres, stride;
_Spheres.GetDimensions(numSpheres, stride);
for (uint i = 0; i < numSpheres; i++)
IntersectSphere(ray, bestHit, i);
return bestHit;
}
从摄像机的每个像素发射一条射线 , 我们得到一个默认的击中点
IntersectGroundPlane 和 IntersectSphere 分别是求交算法,获得一条光线对地面和球的击中点信息 。
_Spheres 是 一个球的结构体 list ,里面存放着我们在外层 传入的球体数量和位置,大小等信息 。
我们循环便利这个list ,对每一个球都求交算出 besthit 点
float3 Shade(inout Ray ray, RayHit hit)
{
if (hit.distance < 1.#INF)
{
// 有限距离
}
else
{
// 无限远
}
}
在有限距离中,我们对射中的物体顶点着色
// Calculate chances of diffuse and specular reflection
hit.albedo = min(1.0f - hit.specular, hit.albedo);
float specChance = energy(hit.specular);
float diffChance = energy(hit.albedo);
// Roulette-select the ray's path
float roulette = rand();
if (roulette < specChance)
{
// Specular reflection
ray.origin = hit.position + hit.normal * 0.001f;
float alpha = SmoothnessToPhongAlpha(hit.smoothness);
ray.direction = SampleHemisphere(reflect(ray.direction, hit.normal), alpha);
float f = (alpha + 2) / (alpha + 1);
ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction, f);
}
else if (diffChance > 0 && roulette < specChance + diffChance)
{
// Diffuse reflection
ray.origin = hit.position + hit.normal * 0.001f;
ray.direction = SampleHemisphere(hit.normal, 1.0f);
ray.energy *= (1.0f / diffChance) * hit.albedo;
}
else
{
// Terminate ray
ray.energy = 0.0f;
}
return hit.emission;
albedo 的 值是 从预先的固有色拿出的,但是为了避免 他的他的反射太强 ,如果他反射太强,就会盖过他的固有色。
所以我们用了一个min去做判定 。
计算 energy
float energy(float3 color)
{
return dot(color, 1.0f / 3.0f);
}
能量的计算就是把rgb三个通道做一个平均。
Roulette-select 算法是一种加速的shading 方法,对 Specular reflection 和 Diffuse reflection 采用不同的贡献值去分开处理,而不是全部
其中我们要做的就是更新光线信息,并且对能量进行衰减 。还有要做的事情是:我们需要在半球上均匀分布的随机方向来更新光线方向 。
float3x3 GetTangentSpace( float3 normal )
{
// Choose a helper vector for the cross product
float3 helper = float3(1, 0, 0);
if (abs(normal.x) > 0.99f)
helper = float3(0, 0, 1);
// Generate vectors
float3 tangent = normalize(cross(normal, helper));
float3 binormal = normalize(cross(normal, tangent));
return float3x3(tangent, binormal, normal);
}
float3 SampleHemisphere( float3 normal, float alpha)
{
// Sample the hemisphere, where alpha determines the kind of the sampling
float cosTheta = pow(rand(), 1.0f / (alpha + 1.0f));
float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
float phi = 2 * PI * rand();
float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
// Transform direction to world space
return mul(tangentSpaceDir, GetTangentSpace(normal));
}
3. path tracing all path loop
- 从屏幕uv坐标
_Pixel = id.xy;
uint width, height;
Result.GetDimensions(width, height);
// Transform pixel to [-1,1] range
float2 uv = float2((id.xy + _PixelOffset ) / float2(width, height) * 2.0f - 1.0f);
为了对每个像素做多点采样 我们再每个uv坐标内进行了多点的随机采样
其中的 _PixelOffset 是由 c# 层 随机值传入的
RayTracingShader.SetVector("_PixelOffset", new Vector2(Random.value, Random.value));
- 创建uv坐标的光线
// Get a ray for the UVs
Ray ray = CreateCameraRay(uv);
// Write some colors
float3 result = float3(0, 0, 0);
- 反射次数:
// 反射次数
for (int i = 0; i < 8 ; i++)
{
RayHit hit = Trace(ray); // 最开始的ray 是从 Camera 开始的
// shade的 ray 和 hit 都是 inout 参数
result += ray.energy * Shade(ray, hit);
// 光线最多反射循环次数 ,如果中间 ray的 energy 的 xyz 有一个为 0 了 ,则它不会再反弹了
if (!any(ray.energy))
break;
}